package uk.co.q3c.deplan.server.domain;

import java.util.ArrayList;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Stack;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;

import uk.co.q3c.deplan.server.util.ReflectionFieldBuilder.FieldTypes;

/**
 * Used by DomainObjectComparator to log differences (instances of difference in value between the two object sets being compared), the
 * total number of fields compared, and if appropriate the rule which was broken. A gwt logger is used to output log details, by default to
 * the console but the log is accessible via the static Log if you wish to configure it differently.<br>
 * <br>
 * The following counts are kept (set by DomainObjectComparator)
 * 
 * 
 * <br>
 * <br>
 * <li>fieldFailCount = the number of fields which failed match value test<br>
 * <br><li>fieldPassCount = the number of fields passed match value test<br>
 * <br><li>fieldErrorCount = the number of fields comparisons which generated an error (see log for detail)<br>
 * <br><li>fieldTotalCount = the total number of fields compared, should be the sum of the above;
 * 
 * The same counts are kept for objects
 * 
 * @author DSowerby 21 Oct 2008
 * 
 */
public class DomainMatchingLog {
	transient protected final Logger logger = Logger.getLogger(getClass().getName());

	public enum Outcome {
		FAIL, ERROR, RULEFAIL, PASS, LOOP, TOTAL, STOP, EXCLUDED;
		int count;

		public static Outcome result(boolean result) {

			return result ? PASS : FAIL;
		}
	}

	public static final EnumSet<Outcome> failSet = EnumSet.of(Outcome.FAIL, Outcome.ERROR, Outcome.RULEFAIL, Outcome.EXCLUDED);

	transient private Object currentMatchA;
	transient private Object currentMatchB;
	transient private final Stack<Object> stack;
	transient private final Stack<String> fieldNameStack;
	ArrayList<Object> objectsChecked = new ArrayList<Object>();

	EnumSet<Outcome> logOutcomes = EnumSet.copyOf(DomainMatchingLog.failSet);
	EnumSet<FieldTypes> logItems = EnumSet.of(FieldTypes.PRIMITIVE, FieldTypes.STRING, FieldTypes.ENUM);

	boolean loopWarning = false;

	EnumMap<FieldTypes, EnumMap<Outcome, AtomicInteger>> items;

	String failedRule = "none";

	private int totalFails;

	private final int maxFails = 50;

	public DomainMatchingLog() {
		createFieldTypes();
		stack = new Stack<Object>();
		fieldNameStack = new Stack<String>();
		// Layout layout = new PatternLayout();
		// try {
		// Log.addAppender(new FileAppender(layout,
		// "/home/dave/temp/compare.log"));
		// } catch (IOException e) {
		// // TODO Auto-generated catch block
		// e.printStackTrace();
		// }
	}

	private void createFieldTypes() {
		items = new EnumMap<FieldTypes, EnumMap<Outcome, AtomicInteger>>(FieldTypes.class);
		for (FieldTypes item : FieldTypes.values()) {
			EnumMap<Outcome, AtomicInteger> value = new EnumMap<Outcome, AtomicInteger>(Outcome.class);
			for (Outcome outcome : Outcome.values()) {
				value.put(outcome, new AtomicInteger(0));
			}
			items.put(item, value);
		}

	}

	public void add(String message) {
		logger.info(message);

	}

	public void add(FieldTypes item, Outcome outcome, Object objectA, Object objectB) {

		String valueA;
		String valueB;
		try {
			valueA = currentMatchA.toString();
		} catch (Exception e) {
			valueA = "null";
		}
		try {
			valueB = currentMatchB.toString();
		} catch (Exception e) {
			valueB = "null";
		}

		if ((logItems.contains(item) && (logOutcomes.contains(outcome)))) {
			String msg = outcome.toString() + ": " + objectPath() + " value A=" + valueA + " value B=" + valueB;
			add(msg);

		}

	}

	public String classPath() {
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < stack.size(); i++) {
			if (stack.get(i) == null) {
				sb.append("null");
			} else {
				sb.append(stack.get(i).getClass().getSimpleName());
			}
			if (i != stack.size() - 1) {
				sb.append("/");
			}
		}
		return sb.toString();
	}

	public String objectPath() {
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < fieldNameStack.size(); i++) {
			if (fieldNameStack.get(i) == null) {
				sb.append("null");
			} else {
				sb.append(fieldNameStack.get(i));
			}
			if (i != fieldNameStack.size() - 1) {
				sb.append("/");
			}
		}
		return sb.toString();

	}

	public int get(FieldTypes item, Outcome outcome) {
		return items.get(item).get(outcome).get();
	}

	public String getFailedRule() {
		return failedRule;
	}

	public void setFailedRule(String failedRule) {
		this.failedRule = failedRule;
	}

	public void clear() {
		createFieldTypes();
		stack.clear();
		fieldNameStack.clear();
		objectsChecked.clear();
		totalFails = 0;
	}

	public Object getCurrentMatchA() {
		return currentMatchA;
	}

	// public void setCurrentMatchA(Object currentMatchA) {
	// this.currentMatchA = currentMatchA;
	// }

	public Object getCurrentMatchB() {
		return currentMatchB;
	}

	// public void setCurrentMatchB(Object currentMatchB) {
	// this.currentMatchB = currentMatchB;
	// }

	public boolean startObject(Object objectA, Object objectB, String fieldName) {
		if (objectA == null) {
			loopWarning = false;
		} else {
			loopWarning = stack.contains(objectA);
		}
		stack.push(objectA);
		currentMatchA = objectA;
		currentMatchB = objectB;
		fieldNameStack.push(fieldName);
		// System.out.println("==========> start object " + objectPath());
		return true;
	}

	public Object endObject(FieldTypes item, Outcome outcome, Object objectA, Object objectB) {
		Outcome filteredOutcome = outcome;

		add(item, filteredOutcome, objectA, objectB);
		items.get(item).get(filteredOutcome).incrementAndGet();
		items.get(item).get(Outcome.TOTAL).incrementAndGet();
		if (failSet.contains(filteredOutcome)) {
			totalFails++;
		}
		// System.out.println("<========== end object " + objectPath());
		fieldNameStack.pop();
		return stack.pop();
	}

	public void addError(String msg) {
		add("ERROR: " + msg);

	}

	public void report() {
		System.out.println(" ~~~~~~~~~~~~~~~~ report start ~~~~~~~~~~~~~~~~~~~");
		for (FieldTypes item : FieldTypes.values()) {
			for (Outcome outcome : Outcome.values()) {
				AtomicInteger ai = items.get(item).get(outcome);
				System.out.println(item.toString() + " " + outcome.toString() + " = " + ai.toString());
			}
			System.out.println();
		}
		System.out.println(" ~~~~~~~~~~~~~~~~ report end ~~~~~~~~~~~~~~~~~~~");
	}

	public boolean stackContains(Object objectA) {
		return stack.contains(objectA);
	}

	public boolean hasLoopWarning() {
		return loopWarning;
	}

	public EnumSet<Outcome> getLogOutcomes() {
		return logOutcomes;
	}

	public void setLogOutcomes(EnumSet<Outcome> logOutcomes) {
		this.logOutcomes = logOutcomes;
	}

	public EnumSet<FieldTypes> getLogItems() {
		return logItems;
	}

	public void setLogItems(EnumSet<FieldTypes> logItems) {
		this.logItems = logItems;
	}

	public void addLogItem(FieldTypes item) {
		logItems.add(item);

	}

	public void logAll() {
		logItems = EnumSet.range(FieldTypes.PRIMITIVE, FieldTypes.UNDEFINEDOBJECT);
		logOutcomes = EnumSet.range(Outcome.FAIL, Outcome.LOOP);
	}

	public void logAllFail() {
		logItems = EnumSet.range(FieldTypes.PRIMITIVE, FieldTypes.UNDEFINEDOBJECT);
		logOutcomes = EnumSet.copyOf(DomainMatchingLog.failSet);
	}

	public int getTotalFails() {
		return totalFails;
	}

	public boolean maxFailsExceeded() {
		return totalFails > maxFails;
	}

	public String fieldNameStack() {
		return fieldNameStack.toString();
	}
}